﻿
/***************************************************************************

Copyright (c) Microsoft Corporation. All rights reserved.
This code is licensed under the Visual Studio SDK license terms.
THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

***************************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Xml.Linq;
using System.Diagnostics;
using System.Data.Metadata.Edm;
using Microsoft.Win32;

namespace CodeGenerator.Mvvm.ViewModelGenerator
{
    // In order to be compatible with this single file generator, the input file has to
    // follow the schema in XMLClassGeneratorSchema.xsd

    /// <summary>
    /// Generates source code based on a XML document
    /// </summary>
    public static class SourceCodeGenerator
    {
        /// <summary>
        /// Create a CodeCompileUnit based on the XmlDocument doc
        /// In order to be compatible with this single file generator, the input XmlDocument has to
        /// follow the schema in XMLClassGeneratorSchema.xsd
        /// </summary>
        /// <param name="doc">An XML document that contains the description of the code to be generated</param>
        /// <param name="namespaceName">If the root node of doc does not have a namespace attribute, use this instead</param>
        /// <returns>The generated CodeCompileUnit</returns>
        public static CodeCompileUnit CreateCodeCompileUnit(ViewModelGenerationOptions doc, ProjectItemInfo projInfo, string namespaceName)
        {

            // Extract the entities from the CSDL

            CodeNamespace codeNamespace = new CodeNamespace(namespaceName);
            XElement edmxElement = projInfo.GetCSDL();
            ViewModelEntityOption entityOptions;

            string commonBaseClass = doc.CommonBaseClass;


            AddNamespaces(codeNamespace, projInfo, doc);
            string entityName;

            foreach (var xType in edmxElement.Elements("{http://schemas.microsoft.com/ado/2006/04/edm}EntityType"))            
            {
                entityName = xType.Attribute("Name").Value;
                entityOptions = null;

                if (doc.Entities.Exists(x => x.Name == entityName))
                {
                    entityOptions = doc.Entities.Find(x => x.Name == entityName);
                }

                CodeTypeDeclaration viewModelClass = CreateClass(xType, entityOptions, commonBaseClass);
                codeNamespace.Types.Add(viewModelClass);
            }

            CodeCompileUnit code = new CodeCompileUnit();           
            // Just for VB.NET:
            // Option Strict On (controls whether implicit type conversions are allowed)
            code.UserData.Add("AllowLateBound", false);
            // Option Explicit On (controls whether variable declarations are required)
            code.UserData.Add("RequireVariableDeclaration", true);

            
            code.Namespaces.Add(codeNamespace);
            return code;
        }

        private static void AddNamespaces(CodeNamespace codeNamespace, ProjectItemInfo projInfo, ViewModelGenerationOptions doc)
        {
            string edmxReference = projInfo.GetNamespaceReference();

            if (!string.IsNullOrEmpty(edmxReference))
            {
                codeNamespace.Imports.Add(new CodeNamespaceImport(edmxReference));
            }

            if (doc.Namespaces != null)
            {
                foreach (NamespaceInclude ni in doc.Namespaces)
                {
                    codeNamespace.Imports.Add(new CodeNamespaceImport(ni.Value));
                }
            }

        }

        private static CodeTypeDeclaration CreateClass(XElement xType, ViewModelEntityOption entityOption, string commonBaseClass)
        {

            // determine the name of the class
            string entityName = xType.Attribute("Name").Value;
            string className = string.Concat(entityName, "ViewModel");
            bool useCustomBaseClass = false;
            
            CodeTypeDeclaration returnClass = new CodeTypeDeclaration(className);
            

            returnClass.IsClass = true;
            returnClass.IsPartial = true;

           // If the commonBaseClass is specified, but the entityOption.BaseClass is not, then use it.
            // otherwise, apply the common base class.

            if (entityOption != null)
            {
                if (!string.IsNullOrEmpty(entityOption.BaseClass))
                {
                    returnClass.BaseTypes.Add(new CodeTypeReference(entityOption.BaseClass));
                    useCustomBaseClass = true;
                }
            }

            if (!useCustomBaseClass && !string.IsNullOrEmpty(commonBaseClass))
            {
                returnClass.BaseTypes.Add(new CodeTypeReference(commonBaseClass));
            }


            // Add a partial method declaraction (C# only implementation - needs to be updated for VB)
            //TODO Update for VB.NET
            string partialMethod = string.Concat(entityName, "PropertyChanged");
            string partialMethodSig = string.Concat(partialMethod, "(string propName)");

            CodeMemberField memberField = new CodeMemberField(new CodeTypeReference("partial void"), partialMethodSig);
            memberField.Attributes = MemberAttributes.ScopeMask;
            returnClass.Members.Add(memberField);

            // Add the entity field declaration
            string entityFieldName = string.Concat("_", entityName[0].ToString().ToLower(), entityName.Substring(1, entityName.Length - 1));
            CodeTypeReference refType = new CodeTypeReference(entityName);
            CodeMemberField entityField = new CodeMemberField(refType, entityFieldName);            
            entityField.Name = entityFieldName;
            returnClass.Members.Add(entityField);


            // Add the constructor
            CodeConstructor constructor = new CodeConstructor();
            constructor.Attributes = MemberAttributes.Public;
            returnClass.Members.Add(constructor);

            // Locate the property keys
            var keyElement = xType.Elements("{http://schemas.microsoft.com/ado/2006/04/edm}Key");
            string propName;
            ViewModelPropertyOptions entityProperty;

            // Add a new member based on the property types.            
            foreach (var propElement in xType.Elements("{http://schemas.microsoft.com/ado/2006/04/edm}Property"))
            {
                propName = propElement.Attribute("Name").Value;
                entityProperty = null;

                // Determine if a property directive is specified.
                if (entityOption != null)
                {
                   

                    if (entityOption.EntityProperties != null)
                    {
                        if (entityOption.EntityProperties.Exists(x => String.Compare(x.Name, propName, true) == 0)) 
                        {
                            entityProperty = entityOption.EntityProperties.Find(x => String.Compare(x.Name, propName, true) == 0);
                        }
                    }
                }


                try
                {
                    AddProperty(returnClass, propElement, entityFieldName, partialMethod, entityProperty);
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("Error generating property {0} for class {1}", propElement.Attribute("Name").Value, className), ex);
                }
            }


            return returnClass;
        }

        private static void AddProperty(CodeTypeDeclaration returnClass, XElement propElement, string entityFieldName, string partialMethod, ViewModelPropertyOptions propertyOptions)
        {

            CodeMemberProperty codeProp = new CodeMemberProperty();
            string propName = propElement.Attribute("Name").Value;
            string typeName = propElement.Attribute("Type").Value;
            string internalPropName = string.Concat(entityFieldName, ".", propName);
            PrimitiveType primType;
            bool? isNullableTry = propElement.Attribute("Nullable") == null ? null : (bool?)bool.Parse(propElement.Attribute("Nullable").Value);
            bool isNullable;
            codeProp.Name = propName;
            codeProp.Attributes = MemberAttributes.Public;

            primType = GetPrimitiveType(typeName);

            if (isNullableTry.HasValue)
            {
                isNullable = GetNullability(isNullableTry.Value, primType);
            }
            else
            {
                isNullable = GetDefaultNullability(primType);
            }


            // Determine the type of the property 
            codeProp.Type = GetPropertyType(primType, isNullable);

            codeProp.HasGet = true;
            codeProp.HasSet = true;


            // Create the Get statement.
            CodeMethodReturnStatement getReturn = new CodeMethodReturnStatement(new CodeArgumentReferenceExpression(internalPropName));
            codeProp.GetStatements.Add(getReturn);

            // Create the Set statement.
            CodeConditionStatement equalCheck = new CodeConditionStatement();

            // if the value of the set and the value of the container property is the same, 
            // then just exit
            CodeBinaryOperatorExpression binExp = new CodeBinaryOperatorExpression(
                new CodePropertySetValueReferenceExpression(),
                CodeBinaryOperatorType.ValueEquality,
                new CodeVariableReferenceExpression(internalPropName));

            equalCheck.Condition = binExp;

            // if value is the same, just exit.
            equalCheck.TrueStatements.Add(new CodeMethodReturnStatement());

            // Assign the value to the contained property
            CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), internalPropName);
            CodeAssignStatement valueAssign = new CodeAssignStatement(fieldRef, new CodePropertySetValueReferenceExpression());

            // Add a method call to the partialMethod to notify of the property change
            CodeMethodInvokeExpression partialInvoke = new CodeMethodInvokeExpression(
                new CodeThisReferenceExpression(),
                partialMethod,
                new CodeExpression[] { new CodePrimitiveExpression(propName) });

            equalCheck.FalseStatements.Add(valueAssign);
            equalCheck.FalseStatements.Add(partialInvoke);

            codeProp.SetStatements.Add(equalCheck);

            if (propertyOptions != null)
            {
                ApplyPropertyAttributes(codeProp, propertyOptions.Attributes);
            }


            returnClass.Members.Add(codeProp);

        }

        private static void ApplyPropertyAttributes(CodeMemberProperty codeProp, List<ViewModelAttributeOption> attributeOptions)
        {

            if (attributeOptions != null)
            {
                foreach (ViewModelAttributeOption attributeOption in attributeOptions)
                {
                    CodeAttributeDeclaration codeAttrib = new CodeAttributeDeclaration(attributeOption.Class);

                    if (attributeOption.Parameters != null)
                    {
                        foreach (ViewModelAttributeParameter viewModelParam in attributeOption.Parameters)
                        {

                            CodeExpression attribParam = GetAttributeParameter(viewModelParam);
                            
                            codeAttrib.Arguments.Add(new CodeAttributeArgument(attribParam));
                        }
                    }

                    codeProp.CustomAttributes.Add(codeAttrib);

                    //attributeOption.Class;
                }
            }
        }

        private static CodeExpression GetAttributeParameter(ViewModelAttributeParameter attribParam)
        {
            CodeExpression returnExpression = default(CodeExpression);


            if (string.Compare(attribParam.Type, "System.DateTime", true) == 0)
            {
               // DateTime dt = new DateTime(1990, 12, 4, 24, 30, 34, 345, DateTimeKind.Local);
                CodeExpressionStatement codeStatement = new CodeExpressionStatement();
                

                DateTime paramDate = DateTime.Parse(attribParam.Value);
                string kindString = paramDate.Kind.ToString();

                returnExpression = new CodeObjectCreateExpression(
                    new CodeTypeReference(new CodeTypeParameter("System.DateTime")),
                    new CodePrimitiveExpression(paramDate.Year),
                    new CodePrimitiveExpression(paramDate.Month),
                    new CodePrimitiveExpression(paramDate.Day),
                    new CodePrimitiveExpression(paramDate.Hour),
                    new CodePrimitiveExpression(paramDate.Minute),
                    new CodePrimitiveExpression(paramDate.Second),
                    new CodePrimitiveExpression(paramDate.Millisecond),
                    new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(System.DateTimeKind)), kindString));

            }
            else
            {

                object returnObject = null;


                try
                {
                    Type paramType = Type.GetType(attribParam.Type);

                    ConstructorInfo conInfo = paramType.GetConstructor(new Type[] { Type.GetType("System.Char[]") });
                    if (conInfo == null)
                    {

                        MemberInfo[] parseMethods = paramType.GetMember("Parse");
                        if (parseMethods.Length == 0)
                        {
                            // pass the value as the default constructor



                            returnObject = Activator.CreateInstance(paramType, attribParam.Value);

                            //throw new Exception(string.Format("No static parse method found on type {0}", attribParam.Type));
                        }
                        else
                        {
                            returnObject = paramType.InvokeMember("Parse",
                                BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod,
                                Type.DefaultBinder,
                                null,
                                new object[] { attribParam.Value });
                        }

                    }
                    else
                    {
                        returnObject = conInfo.Invoke(new object[] { attribParam.Value.ToCharArray() });
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("Error creating object for type {0} with value {1}", attribParam.Type, attribParam.Value), ex);

                }

                returnExpression = new CodePrimitiveExpression(returnObject);
            }

            return returnExpression;
        }


        private static bool GetNullability(bool isNullable, PrimitiveType primType)
        {
            // since strings are nullable there is no need to mark it as nullable explicitly

            bool returnNullability;

            switch (primType.PrimitiveTypeKind)
            {
                case PrimitiveTypeKind.String:
                    returnNullability = false;
                    break;
                default:
                    returnNullability = isNullable;
                    break;
            }

            return returnNullability;
        }

        private static bool GetDefaultNullability(PrimitiveType primType)
        {
            bool isNullable;

            switch (primType.PrimitiveTypeKind)
            {
                case PrimitiveTypeKind.Binary:
                case PrimitiveTypeKind.String:                
                    isNullable = false;
                    break;
                default:
                    isNullable = true;
                    break;
            }

            return isNullable;
        }

        private static CodeTypeReference GetPropertyType(PrimitiveType primType, bool isNullable)
        {
            CodeTypeReference codeRef = default(CodeTypeReference);


            if (isNullable)
            {
                codeRef = new CodeTypeReference(typeof(Nullable<>), CodeTypeReferenceOptions.GlobalReference);
                codeRef.TypeArguments.Add(primType.ClrEquivalentType);
            }
            else
            {

                codeRef = new CodeTypeReference(primType.ClrEquivalentType);
            }

            return codeRef;
        }

        private static PrimitiveType GetPrimitiveType(string typeName)
        {
            PrimitiveTypeKind typeKind = (PrimitiveTypeKind)Enum.Parse(typeof(PrimitiveTypeKind), typeName);
            PrimitiveType primType = PrimitiveType.GetEdmPrimitiveType(typeKind);
          
            return primType;
        }



        //public decimal? OriginalBalance
        //{
        //    get { return _loan.OriginalBalance; }
        //    set
        //    {
        //        if (value == _loan.OriginalBalance)
        //            return;

        //        _loan.OriginalBalance = value;

        //        base.OnPropertyChanged("OriginalBalance");
        //    }
        //}


    }
}